package cz.muni.fi.xvano1.trinity.core;

import cz.muni.fi.xvano1.trinity.supplementary.CalendarHandler;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.text.ParseException;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateTime;

/**
 * This class has instance independent validations that
 * cz.muni.fi.xvano1.trinity.core.Core class needs.
 *
 * @author Jan Vano
 */
public class SecondaryParser {

    /**
     * Method for creating simplified version of FlexNet Publisher license file
     * (license.dat). Method takes the directory where license.dat file should
     * be and generates simplified version for internal use called limits.dat.
     *
     * @return Integer specifying result state: 0 - passed correctly and
     * limits.dat file was created; 1 - null pointer call; 2 - target directory
     * does not exist or is not a directory; 3 - license.dat could not be read
     * due to I/O error; 4 - limits.dat could not be written due to I/O error
     */
    public static int parseLicenseFile(String dir) {
        File directory = new File(dir);
        if (dir == null) {
            return 1;
        }
        if (!(directory.exists())) {
            return 2;
        }
        if (!(directory.isDirectory())) {
            return 2;
        }
        BufferedReader input;
        try {
            input = new BufferedReader(new FileReader(dir.toString() + "/license.dat"));
        } catch (FileNotFoundException ex) {
            Logger.getLogger(SecondaryParser.class.getName()).log(Level.SEVERE, null, ex);
            return 3;
        }
        BufferedWriter output;
        try {
            output = new BufferedWriter(new FileWriter(dir.toString() + "/limits.dat"));
        } catch (IOException ex) {
            Logger.getLogger(SecondaryParser.class.getName()).log(Level.SEVERE, null, ex);
            return 4;
        }
        String in;
        String[] temp;
        String out;
        try {
            while ((in = input.readLine()) != null) {
                temp = in.split(" ");
                if ((temp[0].equals("FEATURE")) || (temp[0].equals("INCREMENT"))) {
                    out = temp[1] + " " + temp[5];
                    output.write(out + "\n");
                }
            }
        } catch (IOException ex) {
            Logger.getLogger(SecondaryParser.class.getName()).log(Level.SEVERE, null, ex);
            try {
                input.close();
                //output.flush();
                output.close();
            } catch (IOException ex1) {
                Logger.getLogger(SecondaryParser.class.getName()).log(Level.SEVERE, null, ex1);
            }
            return 4;
        }
        try {
            input.close();
            //output.flush();
            output.close();
        } catch (IOException ex) {
            Logger.getLogger(SecondaryParser.class.getName()).log(Level.SEVERE, null, ex);
        }
        return 0;
    }

    /**
     * Method for loading names of modules FlexNet Publisher license file contains.
     * Method takes simplified version of license.dat file called limits.dat and
     * return set of available toolboxes.
     *
     * @return Set of strings representing these names or null if names could
     * not be read.
     */
    public static Set<String> getToolboxNames(String dir) {
        File directory = new File(dir);
        if (dir == null) {
            System.err.println("Null pointer call in getToolboxNames");
            return null;
        }
        if (!(directory.exists())) {
            return null;
        }
        if (!(directory.isDirectory())) {
            return null;
        }
        BufferedReader input;
        try {
            input = new BufferedReader(new FileReader(dir.toString() + "/limits.dat"));
        } catch (FileNotFoundException ex) {
            Logger.getLogger(SecondaryParser.class.getName()).log(Level.WARNING, null, ex);
            if (parseLicenseFile(dir) != 0) {
                return null;
            }
            try {
                input = new BufferedReader(new FileReader(dir.toString() + "/limits.dat"));
            } catch (FileNotFoundException ex1) {
                Logger.getLogger(SecondaryParser.class.getName()).log(Level.SEVERE, null, ex1);
                return null;
            }
        }
        String in;
        String[] temp;
        Set<String> keys = new HashSet<String>();
        try {
            while ((in = input.readLine()) != null) {
                temp = in.split(" ");
                keys.add(temp[0]);
            }
        } catch (IOException ex) {
            Logger.getLogger(SecondaryParser.class.getName()).log(Level.SEVERE, null, ex);
            try {
                input.close();
            } catch (IOException ex1) {
                Logger.getLogger(SecondaryParser.class.getName()).log(Level.SEVERE, null, ex1);
            }
            return null;
        }
        try {
            input.close();
        } catch (IOException ex) {
            Logger.getLogger(SecondaryParser.class.getName()).log(Level.SEVERE, null, ex);
        }
        return keys;
    }

    /**
     * This method validates name, description and location of event. All
     * arguments are validated according to regular expressions in class
     * cz.muni.fi.xvano1.trinity.core.Core.
     *
     * @param name Name of event.
     * @param description Description of event.
     * @param location Location of event.
     * @return Integer number based on the results: 0 - all arguments are valid;
     * 1 - null pointer call on name; 2 - null pointer call on description; 3 -
     * null pointer call on location; 4 - invalid name; 5 - ivalid description;
     * 6 - invalid location
     */
    public static int checkNameDescLoc(String name, String description, String location) {
        if (name == null) {
            System.err.println("Calling checkDate with 'null' name.");
            return 1;
        }
        if (!(name.matches(Core.summaryRegex))) {
            System.err.println("Calling checkDate with invalid name of event.");
            return 4;
        }
        if (description == null) {
            System.err.println("Calling checkDate with 'null' description.");
            return 2;
        }
        if (!(description.matches(Core.descriptionRegex))) {
            System.err.println("Calling checkDate with invalid event description.");
            return 5;
        }
        if (location == null) {
            System.err.println("Calling checkDate with 'null' location.");
            return 3;
        }
        if (!(location.matches(Core.summaryRegex))) {
            System.err.println("Calling checkDate with invalid event location.");
            return 6;
        }
        return 0;
    }

    /**
     * Method validates whether input data are sufficient and/or valid for event
     * creation. Dates from yesterday to 1 year in the future can be used.
     *
     * @param year Year of event star/end
     * @param month Month of event start/end
     * @param day Day of event start/end
     * @return Number based on whether validation was successful or not: 0 -
     * successful; 1 - invalid year; 2 - invalid month; 3 - invalid day; 4 -
     * wrong day with respect to month (eg. 30th February); 5 - boundary of
     * start/end day before yesterday; 6 - boundary of start/end day more than 1
     * year ahead of today;
     */
    public static int checkDate(int year, int month, int day) {
        int length = Integer.toString(year).length();
        if (length != 4) {
            System.err.println("Calling checkDate with wrong year value. It has to have 4 digits.");
            return 1;
        }
        if (!(month >= 1 && month <= 12)) {
            System.err.println("Calling checkDate with wrong value of month.");
            return 2;
        }
        if (!(day >= 1 && day <= 31)) {
            System.err.println("Calling checkDate with wrong value of day.");
            return 3;
        }
        switch (month) {
            case 2:
                if (day > 29) {
                    System.err.println("Calling checkDate with wrong value of day with respect to month.");
                    return 4;
                }
            case 4:
                if (day == 31) {
                    System.err.println("Calling checkDate with wrong value of day with respect to month.");
                    return 4;
                }
            case 6:
                if (day == 31) {
                    System.err.println("Calling checkDate with wrong value of day with respect to month.");
                    return 4;
                }
            case 9:
                if (day == 31) {
                    System.err.println("Calling checkDate with wrong value of day with respect to month.");
                    return 4;
                }
            case 11:
                if (day == 31) {
                    System.err.println("Calling checkDate with wrong value of day with respect to month.");
                    return 4;
                }
        }

        java.util.Calendar yesterday = java.util.Calendar.getInstance();
        yesterday.add(java.util.Calendar.DAY_OF_MONTH, -1);
        yesterday.set(java.util.Calendar.HOUR_OF_DAY, 0);
        yesterday.set(java.util.Calendar.MINUTE, 0);
        yesterday.set(java.util.Calendar.SECOND, 0);
        yesterday.set(java.util.Calendar.MILLISECOND, 0);
        java.util.Calendar testDay = java.util.Calendar.getInstance();
        testDay.set(year, month - 1, day);
        testDay.set(java.util.Calendar.HOUR_OF_DAY, 0);
        testDay.set(java.util.Calendar.MINUTE, 0);
        testDay.set(java.util.Calendar.SECOND, 0);
        testDay.set(java.util.Calendar.MILLISECOND, 0);
        if (!(yesterday.getTimeInMillis() <= testDay.getTimeInMillis())) {
            System.err.println("Called checkDate with event that has a boundary day before yesterday.");
            return 5;
        }
        java.util.Calendar threeYearsAheadDay = java.util.Calendar.getInstance();
        threeYearsAheadDay.add(java.util.Calendar.YEAR, 1);
        threeYearsAheadDay.set(java.util.Calendar.HOUR_OF_DAY, 23);
        threeYearsAheadDay.set(java.util.Calendar.MINUTE, 59);
        threeYearsAheadDay.set(java.util.Calendar.SECOND, 59);
        threeYearsAheadDay.set(java.util.Calendar.MILLISECOND, 999);
        if (!(testDay.getTimeInMillis() <= threeYearsAheadDay.getTimeInMillis())) {
            System.err.println("Called checkDate with event that has a boundary day more than 1 year in future.");
            return 6;
        }
        return 0;
    }

    /**
     * Validations of date-time format.
     *
     * @param dateTime Date-time which should be in format DD.MM.YYYY HH:MM
     * @return Integer number explaining the validation results: 0 - date-time
     * is ok 1 - null pointer call 2 - date-time does not match DD.MM.YYYY HH:MM
     * pattern 3 - date part wrong 4 - time part wrong
     */
    public static int checkSingleDateTime(String dateTime) {
        if (dateTime == null) {
            return 1;
        }
        if (!dateTime.matches(Core.dateRegex + " " + Core.timeRegex)) {
            return 2;
        }
        String date = dateTime.split(" ")[0];
        int year = Integer.parseInt(date.substring(6, 10));
        int month = Integer.parseInt(date.substring(3, 5));
        int day = Integer.parseInt(date.substring(0, 2));
        if (checkDate(year, month, day) != 0) {
            return 3;
        }
        String time = dateTime.split(" ")[1];
        if (checkSingleTime(time) != 0) {
            return 4;
        }
        return 0;
    }

    /**
     * This method checks start and end date-times.
     *
     * @param start Start date-time
     * @param end End date-time
     * @return 0 if both date-times are correct. Otherwise first non-zero error
     * code based on checkSingleDateTime. If formats are correct but the time from start
     * to end would be negative or none, error code 4 is returned.
     */
    public static int checkDateTime(String start, String end) {
        if ((checkSingleDateTime(start) == 1) || (checkSingleDateTime(end) == 1)) {
            return 1;
        }
        if ((checkSingleDateTime(start) == 2) || (checkSingleDateTime(end) == 2)) {
            return 2;
        }
        if ((checkSingleDateTime(start) == 3) || (checkSingleDateTime(end) == 3)) {
            return 3;
        }
        if ((checkSingleDateTime(start) == 3) || (checkSingleDateTime(end) == 3)) {
            return 4;
        }
        int checkTimeresult = checkTime(start, end);
        if (checkTimeresult != 0) {
            return 3 + checkTimeresult;
        }
        return 0;
    }

    /**
     * Method parses date string for DSTART/DTEND line from iCalendar format and
     * loads values from this string for display.
     *
     * @param date Date or date-time value from iCalendar format.
     * @return String array in format {name;year;month;day;hour;minute}. Hour
     * and minute fields are empty if the param is Date.
     */
    public static String[] parseDate(String date) {
        try {
            Date tempDate = new Date(date);
        } catch (ParseException ex) {
            //There is high possibility of an exception due to parsing Date-Time as a Date.
            //Logger.getLogger(SecondaryParser.class.getName()).log(Level.WARNING, null, ex);
            return parseDateTime(date);
        }
        if (date.length() != 8) {
            return null;
        }
        for (int i = 0; i <= 7; i++) {
            try {
                Integer.parseInt(String.valueOf(date.charAt(i)));
            } catch (NumberFormatException ex) {
                return null;
            }
        }
        String[] dateValues = new String[4];
        dateValues[0] = "Date";
        dateValues[1] = String.valueOf(date.toCharArray(), 0, 4);
        dateValues[2] = String.valueOf(date.toCharArray(), 4, 2);
        dateValues[3] = String.valueOf(date.toCharArray(), 6, 2);
        dateValues[4] = "";
        dateValues[5] = "";
        return dateValues;
    }
    /*
     * Called when parseDate cannot parse it's argument due to possible
     * date-time present.
     */
    private static String[] parseDateTime(String date) {
        try {
            DateTime tempDate = new DateTime(date);
        } catch (ParseException ex) {
            Logger.getLogger(SecondaryParser.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
        int length = date.length();
        if ((length < 15) || (length > 16)) {
            return null;
        }
        for (int i = 0; i <= 14; i++) {
            if (i == 8) {
                if (date.charAt(i) == "T".charAt(0)) {
                    continue;
                } else {
                    return null;
                }
            }
            try {
                Integer.parseInt(String.valueOf(date.charAt(i)));
            } catch (NumberFormatException ex) {
                return null;
            }
        }
        if (length == 16) {
            if (date.charAt(15) != "Z".charAt(0)) {
                return null;
            }
        }
        String[] dateValues = new String[6];
        dateValues[0] = "DateTime";
        dateValues[1] = String.valueOf(date.toCharArray(), 0, 4);
        dateValues[2] = String.valueOf(date.toCharArray(), 4, 2);
        dateValues[3] = String.valueOf(date.toCharArray(), 6, 2);
        dateValues[4] = String.valueOf(date.toCharArray(), 9, 2);
        dateValues[5] = String.valueOf(date.toCharArray(), 11, 2);
        return dateValues;
    }

    /**
     * Method checks whether a string is a valid time representation.
     * @param time String representing time
     * @return Number based on state of input parameter:
     * 0 - ok
     * 1 - null pointer call
     * 2 - time not in HH:MM format
     * 3 - hour out of range (0-23) or minute out of range (0-59)
     */
    public static int checkSingleTime(String time) {
        if (time == null) {
            System.out.println("Null pointer in checkTime call!");
            return 1;
        }
        if (time.length() != 5) {
            System.out.println("Times need to be in hh:mm format!");
            return 2;
        }
        int timeHour;
        char timeChar;
        int timeMinute;
        try {
            timeHour = Integer.parseInt(String.valueOf(time.toCharArray(), 0, 2));
            timeChar = time.charAt(2);
            timeMinute = Integer.parseInt(String.valueOf(time.toCharArray(), 3, 2));
        } catch (NumberFormatException numberFormatException) {
            System.out.println("Times need to be in hh:mm format!");
            return 2;
        }
        if (timeChar != ":".charAt(0)) {
            System.out.println("Times need to be in hh:mm format!");
            return 2;
        }
        if ((!(timeHour >= 0 && timeHour <= 23))
                || (!(timeMinute >= 0 && timeMinute <= 59))) {
            System.out.println("Valid hours are 0-23 and valid minutes are 0-59!");
            return 3;
        }
        return 0;
    }

    /**
     * Method checks if the start hour is less than end hour and if duration is positive.
     * @param start Start date-time
     * @param end End date-tim
     * @return 0 if duration is positive, 1 if date-times does not match
     * DD.MM.YYYY HH:MM pattern or 2 if the duration was zero or negative
     */
    public static int checkTime(String start, String end) {
        if (!(start.matches(Core.dateRegex + " " + Core.timeRegex))
                || !(end.matches(Core.dateRegex + " " + Core.timeRegex))) {
            return 1;
        }
        Calendar startCal = CalendarHandler.getCalendarForDateTime(start);
        Calendar endCal = CalendarHandler.getCalendarForDateTime(end);
        long startTime = startCal.getTimeInMillis();
        long endTime = endCal.getTimeInMillis();
        if (!(startTime < endTime)) {
            System.out.println("Zero or negative duration!");
            return 2;
        }
        return 0;
    }
}
